home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 1320 / 1320.xpi / components / gmServiceGmail.js < prev    next >
Text File  |  2010-01-22  |  36KB  |  1,071 lines

  1. // Gmail Manager
  2. // By Todd Long <longfocus@gmail.com>
  3. // http://www.longfocus.com/firefox/gmanager/
  4.  
  5. const GM_NOTIFY_STATE = "gmanager-accounts-notify-state";
  6.  
  7. function gmServiceGmail()
  8. {
  9.   // Load the services
  10.   this._logger = Components.classes["@longfocus.com/gmanager/logger;1"].getService(Components.interfaces.gmILogger);
  11.   this._cookies = Components.classes["@longfocus.com/gmanager/cookies;1"].getService(Components.interfaces.gmICookies);
  12.   this._observer = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
  13.   
  14.   if ("@mozilla.org/xre/app-info;1" in Components.classes)
  15.   {
  16.     var platformVersion = Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULAppInfo).platformVersion;
  17.     this._swapCookies = (platformVersion === null || platformVersion < "1.8.1");
  18.   }
  19. }
  20. gmServiceGmail.prototype = {
  21.   _email: null,
  22.   _password: null,
  23.   _isHosted: false,
  24.   _username: null,
  25.   _domain: null,
  26.   _loginURL: null,
  27.   _checkURL: null,
  28.   _status: Components.interfaces.gmIService.STATE_LOGGED_OUT,
  29.   _loggedIn: false,
  30.   _checking: false,
  31.   _inboxUnread: 0,
  32.   _savedDrafts: 0,
  33.   _spamUnread: 0,
  34.   _spaceUsed: null,
  35.   _percentUsed: null,
  36.   _totalSpace: null,
  37.   _labels: null,
  38.   _snippets: null,
  39.   _timer: null,
  40.   _connectionPhase: 0,
  41.   _channel: null,
  42.   _cookies: null,
  43.   _swapCookies: true,
  44.   
  45.   _log: function(aMsg)
  46.   {
  47.     this._logger.log("(" + this.email + ") " + aMsg);
  48.   },
  49.   
  50.   /**
  51.    * gmIServiceGmail
  52.    */
  53.   get isHosted() { return this._isHosted; },
  54.   get username() { return this._username; },
  55.   get domain() { return this._domain; },
  56.   
  57.   /**
  58.    * gmIService
  59.    */
  60.   get email() { return this._email; },
  61.   get status() { return this._status; },
  62.   get loggedIn() { return this._loggedIn; },
  63.   get checking() { return this._checking; },
  64.   get inboxUnread() { return this._inboxUnread; },
  65.   get savedDrafts() { return this._savedDrafts; },
  66.   get spamUnread() { return this._spamUnread; },
  67.   get spaceUsed() { return this._spaceUsed; },
  68.   get percentUsed() { return this._percentUsed; },
  69.   get totalSpace() { return this._totalSpace; },
  70.   
  71.   getInbox: function(aPassword)
  72.   {
  73.     return this._getService(aPassword);
  74.   },
  75.   
  76.   getCompose: function(aPassword, aHref)
  77.   {
  78.     var href = (aHref ? aHref.replace("mailto:", "&to=").replace("subject=", "su=").replace(/ /g, "%20").replace("?", "&") : "");
  79.     return this._getService(aPassword, "view=cm&fs=1" + href);
  80.   },
  81.   
  82.   _getService: function(aPassword, /* Optional */ aContinueData)
  83.   {
  84.     var serviceURI = new Object();
  85.     
  86.     if (aContinueData == undefined)
  87.       aContinueData = "";
  88.     
  89.     serviceURI.url = this._loginURL;
  90.     serviceURI.data = "ltmpl=default<mplcache=2" + 
  91.                       "&continue=" + encodeURIComponent(this._checkURL + aContinueData) + 
  92.                       "&service=mail&rm=false<mpl=default" + 
  93.                       "&Email=" + encodeURIComponent(this.isHosted ? this.username : this.email) + 
  94.                       "&Passwd=" + encodeURIComponent(aPassword) + 
  95.                       "&rmShown=1&signIn=Sign+in";
  96.     
  97.     var xmlHttpRequest = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Components.interfaces.nsIXMLHttpRequest);
  98.     
  99.     // Send the HTTP request
  100.     xmlHttpRequest.open("GET", this._loginURL, false); 
  101.     xmlHttpRequest.send(null);
  102.     
  103.     if (xmlHttpRequest.status == 200)
  104.     {
  105.       var httpChannel = xmlHttpRequest.channel.QueryInterface(Components.interfaces.nsIHttpChannel);
  106.       serviceURI.cookies = this._cookieMonster(httpChannel);
  107.     }
  108.     
  109.     if (serviceURI.cookies && "GALX" in serviceURI.cookies)
  110.       serviceURI.data += "&" + serviceURI.cookies["GALX"].pair;
  111.     
  112.     return serviceURI;
  113.   },
  114.   
  115.   getLabels: function(aCount)
  116.   {
  117.     var labels = new Array();
  118.     if (this._labels)
  119.       labels = this._labels;
  120.     aCount.value = labels.length;
  121.     return labels;
  122.   },
  123.   
  124.   getSnippets: function(aCount)
  125.   {
  126.     var snippets = new Array();
  127.     if (this._snippets)
  128.       snippets = this._snippets;
  129.     aCount.value = snippets.length;
  130.     return snippets;
  131.   },
  132.   
  133.   _fixGmailOffline: function()
  134.   {
  135.     // Fix for Gmail Offline (i.e. cookie management)
  136.     var cookieManager = Components.classes["@mozilla.org/cookiemanager;1"].getService(Components.interfaces.nsICookieManager2);
  137.     var cookieEnumerator = cookieManager.enumerator;
  138.     var cookieName = (this.isHosted ? "GAUSR@" + this.domain : "GAUSR");
  139.     var cookieHost = "mail.google.com";
  140.     var gausrExists = false;
  141.     
  142.     while (cookieEnumerator.hasMoreElements() && !gausrExists)
  143.     {
  144.       var cookie = cookieEnumerator.getNext();
  145.       
  146.       if (cookie instanceof Components.interfaces.nsICookie)
  147.         gausrExists = (cookie.host == cookieHost && cookie.name == cookieName);
  148.     }
  149.     
  150.     if (!gausrExists)
  151.     {
  152.       var cookiePath = (this.isHosted ? "/a/" + this.domain : "/mail");
  153.       
  154.       try {
  155.         cookieManager.add(cookieHost, cookiePath, cookieName, this.email, false, false, true, Math.pow(2, 34));
  156.       } catch(e) {
  157.         cookieManager.add(cookieHost, cookiePath, cookieName, this.email, false, true, Math.pow(2, 34));
  158.       }
  159.     }
  160.   },
  161.   
  162.   init: function(aEmail)
  163.   {
  164.     this._email = aEmail.toLowerCase();
  165.     this._isHosted = (this.email.indexOf("@gmail.com") == -1 && this.email.indexOf("@googlemail.com") == -1);
  166.     this._username = this.email.split("@")[0];
  167.     this._domain = this.email.split("@")[1];
  168.     
  169.     // Check if the email is hosted
  170.     if (this.isHosted)
  171.     {
  172.       this._loginURL = "https://www.google.com/a/" + this.domain + "/LoginAction2";
  173.       this._checkURL = "https://mail.google.com/a/" + this.domain + "/?";
  174.     }
  175.     else
  176.     {
  177.       this._loginURL = "https://www.google.com/accounts/ServiceLoginAuth";
  178.       this._checkURL = "https://mail.google.com/mail/?";
  179.     }
  180.   },
  181.   
  182.   login: function(aPassword)
  183.   {
  184.     const emptyRegExp = /^\s*$/;
  185.     
  186.     // Check if already logged in or checking
  187.     if (this.loggedIn || this.checking)
  188.       return;
  189.     
  190.     // Check if the password is specified
  191.     if (aPassword === null || emptyRegExp.test(aPassword))
  192.     {
  193.       this._defaults();
  194.       this._setChecking(false);
  195.       this._setStatus(Components.interfaces.gmIService.STATE_ERROR_PASSWORD);
  196.     }
  197.     else
  198.     {
  199.       // Get the connection details
  200.       var serviceURI = this._getService(aPassword, "labs=0");
  201.       
  202.       // Setup the cookies
  203.       this._cookies = serviceURI.cookies;
  204.       
  205.       // Save the password in case of connection timeout
  206.       this._password = aPassword;
  207.       
  208.       // Fix the cookies for Gmail Offline
  209.       this._fixGmailOffline();
  210.       
  211.       // Set checking and send the server request
  212.       this._setChecking(true);
  213.       this._serverRequest(serviceURI.url, serviceURI.data);
  214.     }
  215.   },
  216.   
  217.   logout: function()
  218.   {
  219.     this._defaults();
  220.     this._setChecking(false);
  221.     this._setStatus(Components.interfaces.gmIService.STATE_LOGGED_OUT);
  222.   },
  223.   
  224.   check: function()
  225.   {
  226.     // Check if already checking
  227.     if (this.checking)
  228.       return;
  229.     
  230.     // Set checking and send the server request
  231.     this._setChecking(true);
  232.     this._serverRequest(this._checkURL + "labs=0");
  233.   },
  234.   
  235.   notify: function(aTimer)
  236.   {
  237.     // Check if already checking
  238.     if (this.checking)
  239.       this._setRetryError(Components.interfaces.gmIService.STATE_ERROR_TIMEOUT);
  240.     else
  241.     {
  242.       // Check if already logged in
  243.       if (this.loggedIn)
  244.         this.check();
  245.       else
  246.         this.login(this._password);
  247.     }
  248.   },
  249.   
  250.   resetUnread: function()
  251.   {
  252.     // Reset the unread counts
  253.     this._inboxUnread = 0;
  254.     this._spamUnread = 0;
  255.     this._snippets = null;
  256.     
  257.     if (this._labels)
  258.     {
  259.       for (var i = 0; i < this._labels.length; i++)
  260.         this._labels[i].unread = 0;
  261.     }
  262.     
  263.     // Update the status so that any observers get notified 
  264.     // and can update the account details appropriately
  265.     this._setStatus(this.status);
  266.   },
  267.   
  268.   _setStatus: function(aStatus)
  269.   {
  270.     // Notify the observers with the status
  271.     this._status = aStatus;
  272.     this._observer.notifyObservers(null, GM_NOTIFY_STATE, this.email + "|" + this.status);
  273.   },
  274.   
  275.   _setChecking: function(aChecking)
  276.   {
  277.     if (aChecking)
  278.     {
  279.       // Preserve request cookies (prior to Mozilla 1.8.1)
  280.       if (this._swapCookies)
  281.       {
  282.         // Load the Google cookies
  283.         this._cookies.loadSession("google.com");
  284.       }
  285.       
  286.       try {
  287.         // Add the HTTP observers
  288.         this._observer.addObserver(this, "http-on-modify-request", false);
  289.         this._observer.addObserver(this, "http-on-examine-response", false);
  290.       } catch(e) {}
  291.       
  292.       // Set the status connecting
  293.       this._setStatus(Components.interfaces.gmIService.STATE_CONNECTING);
  294.       
  295.       // Reset the connection phase
  296.       this._connectionPhase = 0;
  297.       
  298.       // Start the timeout timer (30 seconds)
  299.       this._startTimer(30000);
  300.     }
  301.     else
  302.     {
  303.       // Preserve request cookies (prior to Mozilla 1.8.1)
  304.       if (this._swapCookies)
  305.       {
  306.         // Restore the Google cookies
  307.         this._cookies.restoreSession("google.com");
  308.       }
  309.       
  310.       try {
  311.         // Remove the HTTP observers
  312.         this._observer.removeObserver(this, "http-on-modify-request");
  313.         this._observer.removeObserver(this, "http-on-examine-response");
  314.       } catch(e) {}
  315.       
  316.       // Stop the timeout timer
  317.       this._stopTimer();
  318.     }
  319.     
  320.     // Set whether checking or not
  321.     this._checking = aChecking;
  322.   },
  323.   
  324.   _startTimer: function(aInterval)
  325.   {
  326.     var interval = parseInt(aInterval);
  327.     
  328.     // Stop the timeout timer
  329.     this._stopTimer();
  330.     
  331.     // Check if the interval is valid
  332.     if (!isNaN(interval) && interval > 0)
  333.     {
  334.       // Start the timeout timer which fires only once
  335.       this._timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
  336.       this._timer.initWithCallback(this, interval, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
  337.     }
  338.   },
  339.   
  340.   _stopTimer: function()
  341.   {
  342.     // Check if the timeout timer is active
  343.     if (this._timer)
  344.     {
  345.       // Kill the timeout timer
  346.       this._timer.cancel();
  347.       this._timer = null;
  348.     }
  349.   },
  350.   
  351.   _setRetryError: function(aStatus)
  352.   {
  353.     this._setChecking(false);
  354.     this._setStatus(aStatus);
  355.     this._startTimer(30000);
  356.   },
  357.   
  358.   _defaults: function()
  359.   {
  360.     // Account details
  361.     this._password = null;
  362.     this._loggedIn = false;
  363.     this._checking = false;
  364.     this._inboxUnread = 0;
  365.     this._savedDrafts = 0;
  366.     this._spamUnread = 0;
  367.     this._spaceUsed = null;
  368.     this._percentUsed = null;
  369.     this._totalSpace = null;
  370.     this._labels = null;
  371.     this._snippets = null;
  372.     
  373.     // Login stuff
  374.     this._connectionPhase = 0;
  375.     this._channel = null;
  376.     this._cookies = null;
  377.   },
  378.   
  379.   _serverRequest: function(aURL, /* Optional */ aData)
  380.   {
  381.     this._log("request URL = " + aURL);
  382.     
  383.     var ioService = Components.classes["@mozilla.org/network/io-service;1"].createInstance(Components.interfaces.nsIIOService);
  384.     var uri = ioService.newURI(aURL, null, null);
  385.     
  386.     // Create the HTTP channel to follow
  387.     this._channel = ioService.newChannelFromURI(uri);
  388.     
  389.     // Check for any POST data
  390.     if (typeof aData === "string")
  391.     {
  392.       var stringInputStream = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
  393.       stringInputStream.setData(aData, aData.length);
  394.       
  395.       var uploadChannel = this._channel.QueryInterface(Components.interfaces.nsIUploadChannel);
  396.       uploadChannel.setUploadStream(stringInputStream, "application/x-www-form-urlencoded", -1);
  397.       
  398.       var httpChannel = this._channel.QueryInterface(Components.interfaces.nsIHttpChannel);
  399.       httpChannel.requestMethod = "POST";
  400.     }
  401.     
  402.     // Create the observer for server response
  403.     var observer = new this.observer(this);
  404.     
  405.     // Open the HTTP channel for server request
  406.     this._channel.notificationCallbacks = observer;
  407.     this._channel.asyncOpen(observer, null);
  408.   },
  409.   
  410.   _cookieMonster: function(aHttpChannel, /* Optional */ aCookies)
  411.   {
  412.     var cookies = aCookies;
  413.     
  414.     if (cookies == null)
  415.       cookies = new Object();
  416.     
  417.     if (aHttpChannel != null)
  418.     {
  419.       try {
  420.         var cookieHeader = aHttpChannel.getResponseHeader("Set-Cookie");
  421.         
  422.         String.prototype.firstMatch = function(aRegExp)
  423.         {
  424.           var firstMatch = null;
  425.           try {
  426.             var match = this.match(aRegExp);
  427.             firstMatch = (match && match.length >= 1 ? match[1] : null);
  428.           } catch(e) {
  429.             /* Regular expression format error */
  430.           }
  431.           return firstMatch;
  432.         }
  433.         
  434.         var rawCookies = cookieHeader.split("\n");
  435.         
  436.         for (var i = 0; i < rawCookies.length; i++)
  437.         {
  438.           var cookiePair = rawCookies[i].firstMatch(/^(.*?)(?=;|$)/);
  439.           var cookieName = cookiePair.firstMatch(/(.*?)=/);
  440.           var cookieValue = cookiePair.firstMatch(/=(.*)/);
  441.           var cookieDomain = rawCookies[i].firstMatch(/Domain=(.*?)(?=;|$)/);
  442.           var cookieExpires = rawCookies[i].firstMatch(/Expires=(.*?)(?=;|$)/);
  443.           var cookiePath = rawCookies[i].firstMatch(/Path=(.*?)(?=;|$)/);
  444.           
  445.           if (cookieDomain === null)
  446.             cookieDomain = aHttpChannel.URI.host;
  447.           
  448.           if (cookieExpires !== null)
  449.             cookieExpires = cookieExpires.replace(/-/g, " ");
  450.           
  451.           this._log("cookiePair = " + cookiePair);
  452.           
  453.           // Check if the cookie has expired
  454.           if (Date.parse(cookieExpires) < Date.now())
  455.             delete cookies[cookieName];
  456.           else
  457.             cookies[cookieName] = { pair: cookiePair, name: cookieName, value: cookieValue, domain: cookieDomain, path: cookiePath };
  458.         }
  459.       } catch(e) {}
  460.     }
  461.     
  462.     return cookies;
  463.   },
  464.   
  465.   observe: function(aSubject, aTopic, aData)
  466.   {
  467.     // Check if this is the channel being followed
  468.     if (aSubject == this._channel)
  469.     {
  470.       var httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);
  471.       
  472.       if (this._cookies === null)
  473.         this._cookies = new Object();
  474.       
  475.       switch (aTopic)
  476.       {
  477.         case "http-on-modify-request":
  478.         {
  479.           // Clears the cookies
  480.           httpChannel.setRequestHeader("Cookie", "", false);
  481.           
  482.           for (var cookieName in this._cookies)
  483.           {
  484.             if (httpChannel.URI.host.indexOf(this._cookies[cookieName].domain) > -1)
  485.               httpChannel.setRequestHeader("Cookie", this._cookies[cookieName].pair, true);
  486.           }
  487.           
  488.           break;
  489.         }
  490.         case "http-on-examine-response":
  491.         {
  492.           // Save the incoming cookies
  493.           this._cookies = this._cookieMonster(httpChannel, this._cookies);
  494.           
  495.           // Clear the incoming cookies (Firefox 2.0, Mozilla 1.8.1)
  496.           httpChannel.setResponseHeader("Set-Cookie", "", false);
  497.           
  498.           break;
  499.         }
  500.       }
  501.     }
  502.   },
  503.   
  504.   callback: function(aData, aRequest)
  505.   {
  506.     // Get the http channel containing our data
  507.     var httpChannel = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel);
  508.     
  509.     try {
  510.       const loginRegExp = /\/.*Login.*/i;
  511.       
  512.       // Get HTTP response information
  513.       var status = httpChannel.responseStatus;
  514.       var path = httpChannel.URI.path;
  515.       
  516.       this._log("connection phase = " + this._connectionPhase);
  517.       this._log("http URI path = " + path);
  518.       this._log("http response status = " + status)
  519.       this._log("data = " + aData);
  520.       
  521.       if (status === null || status !== 200) // Bad status
  522.       {
  523.         // Server error, try again in 30 seconds
  524.         this._setRetryError(Components.interfaces.gmIService.STATE_ERROR_NETWORK);
  525.       }
  526.       else if (loginRegExp.test(path) && this._connectionPhase > 0) // Bad password
  527.       {
  528.         this._defaults();
  529.         this._setChecking(false);
  530.         this._setStatus(Components.interfaces.gmIService.STATE_ERROR_PASSWORD);
  531.       }
  532.     } catch(e) {
  533.       // Network error, try again in 30 seconds
  534.       this._setRetryError(Components.interfaces.gmIService.STATE_ERROR_NETWORK);
  535.     }
  536.     
  537.     // Only continue if we're still checking!
  538.     if (this.checking)
  539.     {
  540.       const globalsRegExp = /var\s+GLOBALS\s*=/i;
  541.       const viewDataRegExp = /var\s+VIEW_DATA\s*=/i;
  542.       
  543.       var isLatest = (globalsRegExp.test(aData) && viewDataRegExp.test(aData));
  544.       
  545.       if (isLatest)
  546.       {
  547.         this._log("Using the latest Gmail version =)");
  548.         this._connectionPhase = 2;
  549.       }
  550.       else
  551.         this._log("Using the old Gmail version =(");
  552.       
  553.       // Ok, everything looks good so far =)
  554.       switch (++this._connectionPhase)
  555.       {
  556.         case 1:
  557.         {
  558.           // Send the server request
  559.           this._serverRequest(this._checkURL + "ui=2");
  560.           break;
  561.         }
  562.         case 2:
  563.         {
  564.           // Send the server request
  565.           this._serverRequest(this._checkURL + "ui=1&view=tl&search=inbox&start=0&init=1");
  566.           break;
  567.         }
  568.         case 3:
  569.         {
  570.           // Try to get the account information...
  571.           
  572.           try {
  573.             // Quota
  574.             var quMatches = JSON.fromString(aData.match(/\["qu",(.|\s)+?]/)[0]);
  575.             this._log("\"qu\" match was " + (quMatches ? "found" : "not found"));
  576.             
  577.             this._spaceUsed = quMatches[1];
  578.             this._totalSpace = quMatches[2];
  579.             this._percentUsed = quMatches[3];
  580.             
  581.             this._log("space used = " + this.spaceUsed);
  582.             this._log("total space = " + this.totalSpace);
  583.             this._log("percent used = " + this.percentUsed);
  584.           } catch(e) {
  585.             this._log("error getting the quota: " + e);
  586.           }
  587.           
  588.           try {
  589.             // Initialize the labels
  590.             this._labels = new Array();
  591.             
  592.             // Check which Gmail version
  593.             if (isLatest)
  594.             {
  595.               // Inbox/Drafts/Spam/Labels
  596.               var ldMatches = JSON.fromString(aData.match(/\["ld",(.|\s)+?(\[|])(\s*]){2}/)[0]);
  597.               this._log("\"ld\" match was " + (ldMatches ? "found" : "not found"));
  598.               
  599.               with (Math) {
  600.                 this._inboxUnread = max(0, ldMatches[1][0][1]);
  601.                 this._savedDrafts = max(0, ldMatches[1][4][1]);
  602.                 this._spamUnread = max(0, ldMatches[1][6][1]);
  603.               }
  604.               
  605.               for (var i = 0; i < ldMatches[2].length; i++)
  606.                 this._labels.push({
  607.                   "name" : ldMatches[2][i][0], 
  608.                   "unread" : ldMatches[2][i][1], 
  609.                   "total" : ldMatches[2][i][2]
  610.                 });
  611.             }
  612.             else
  613.             {
  614.               // Inbox/Drafts/Spam
  615.               var dsMatches = JSON.fromString(aData.match(/\["ds",(.|\s)+?](\s*]){2}/)[0]);
  616.               this._log("\"ds\" match was " + (dsMatches ? "found" : "not found"));
  617.               
  618.               with (Math) {
  619.                 this._inboxUnread = max(0, dsMatches[1][0][1]);
  620.                 this._savedDrafts = max(0, dsMatches[1][1][1]);
  621.                 this._spamUnread = max(0, dsMatches[1][2][1]);
  622.               }
  623.               
  624.               // Labels
  625.               var ctMatches = JSON.fromString(aData.match(/\["ct",(.|\s)+?](\s*]){2}/)[0]);
  626.               this._log("\"ct\" match was " + (ctMatches ? "found" : "not found"));
  627.               
  628.               for (var i = 0; i < ctMatches[1].length; i++)
  629.                 this._labels.push({
  630.                   "name" : ctMatches[1][i][0], 
  631.                   "unread" : ctMatches[1][i][1], 
  632.                   "total" : -1
  633.                 });
  634.             }
  635.             
  636.             this._log("inboxUnread = " + this.inboxUnread);
  637.             this._log("savedDrafts = " + this.savedDrafts);
  638.             this._log("spamUnread = " + this.spamUnread);
  639.             
  640.             if (this._labels.length > 0)
  641.             {
  642.               for (var i = 0; i < this._labels.length; i++)
  643.               {
  644.                 var label = this._labels[i];
  645.                 var hasTotal = (label.total > -1);
  646.                 this._log(label.name + " (" + label.unread + (hasTotal ? " of " + label.total : "") + ")");
  647.               }
  648.             }
  649.             else
  650.               this._log("no labels were found");
  651.           } catch(e) {
  652.             this._log("error getting the inbox/drafts/spam/labels: " + e);
  653.           }
  654.           
  655.           try {
  656.             // Initialize the snippets
  657.             this._snippets = new Array();
  658.             
  659.             // Check which Gmail version
  660.             if (isLatest)
  661.             {
  662.               // Snippets
  663.               var tbMatches = aData.match(/\["tb",(.|\s)+?]\s*]/g);
  664.               this._log("\"tb\" match was " + (tbMatches ? "found" : "not found"));
  665.               
  666.               if (tbMatches === null)
  667.                 tbMatches = new Array();
  668.               
  669.               for (var i = 0; i < tbMatches.length; i++)
  670.               {
  671.                 var snippets = JSON.fromString(tbMatches[i]);
  672.                 var offset = snippets[1];
  673.                 var size = snippets[2];
  674.                 
  675.                 for (var j = 3; j < (size + 3); j++)
  676.                 {
  677.                   // Check if the snippet is unread
  678.                   if (snippets[j][3] == 0)
  679.                   {
  680.                     this._snippets.push({
  681.                       "id" : snippets[j][0], 
  682.                       "unread" : true, 
  683.                       "from" : this._replaceHtmlCodes(this._stripHtml(snippets[j][7])),
  684.                       "subject" : this._replaceHtmlCodes(this._stripHtml(snippets[j][9])),
  685.                       "msg" : this._replaceHtmlCodes(this._stripHtml(snippets[j][10])),
  686.                       "time" : this._replaceHtmlCodes(this._stripHtml(snippets[j][14])),
  687.                       "date" : snippets[j][15]
  688.                     });
  689.                   }
  690.                 }
  691.               }
  692.             }
  693.             else
  694.             {
  695.               // Snippets
  696.               var tMatches = aData.match(/\["t",(.|\s)+?]\s*]/g);
  697.               this._log("\"t\" match was " + (tMatches ? "found" : "not found"));
  698.               
  699.               if (tMatches === null)
  700.                 tMatches = new Array();
  701.               
  702.               for (var i = 0; i < tMatches.length; i++)
  703.               {
  704.                 var snippets = JSON.fromString(tMatches[i]);
  705.                 
  706.                 for (var j = 1; j < snippets.length; j++)
  707.                 {
  708.                   // Check if the snippet is unread
  709.                   if (snippets[j][1] == 1)
  710.                   {
  711.                     this._snippets.push({
  712.                       "id" : snippets[j][0], 
  713.                       "unread" : true, 
  714.                       "from" : this._replaceHtmlCodes(this._stripHtml(snippets[j][4])),
  715.                       "subject" : this._replaceHtmlCodes(this._stripHtml(snippets[j][6])),
  716.                       "msg" : this._replaceHtmlCodes(this._stripHtml(snippets[j][7])),
  717.                       "time" : this._replaceHtmlCodes(this._stripHtml(snippets[j][3])),
  718.                       "date" : snippets[j][12]
  719.                     });
  720.                   }
  721.                 }
  722.               }
  723.             }
  724.             
  725.             if (this._snippets.length > 0)
  726.             {
  727.               this._log(this._snippets.length + " snippet(s) were found");
  728.               
  729.               for (var i = 0; i < this._snippets.length; i++)
  730.               {
  731.                 var snippet = this._snippets[i];
  732.                 
  733.                 this._log("snippet[" + i + "].id = " + snippet.id);
  734.                 this._log("snippet[" + i + "].unread = " + snippet.unread);
  735.                 this._log("snippet[" + i + "].from = " + snippet.from);
  736.                 this._log("snippet[" + i + "].subject = " + snippet.subject);
  737.                 this._log("snippet[" + i + "].msg = " + snippet.msg);
  738.                 this._log("snippet[" + i + "].time = " + snippet.time);
  739.                 this._log("snippet[" + i + "].date = " + snippet.date);
  740.               }
  741.             }
  742.             else
  743.               this._log("no snippets were found");
  744.           } catch(e) {
  745.             this._log("error getting the snippets: " + e);
  746.           }
  747.           
  748.           this._loggedIn = true;
  749.           this._setChecking(false);
  750.           this._setStatus(Components.interfaces.gmIService.STATE_LOGGED_IN);
  751.           
  752.           break;
  753.         }
  754.       }
  755.     }
  756.   },
  757.   
  758.   _stripHtml: function(aData)
  759.   {
  760.     return aData.replace(/(<([^>]+)>)/ig, "");
  761.   },
  762.   
  763.   _replaceHtmlCodes: function(aData)
  764.   {
  765.     var htmlCodes = new Array(
  766.       [">", ">"], ["<", "<"], ["'", "'"], [""", "\""],
  767.       ["&", "&"], ["˜", "~"], ["™", "?"], ["©", "?"],
  768.       ["®", "?"], ["…", ""] );
  769.     
  770.     for (var i = 0; i < htmlCodes.length; i++)
  771.     {
  772.       var regExp = new RegExp(htmlCodes[i][0], "g");
  773.       aData = aData.replace(regExp, htmlCodes[i][1]);
  774.     }
  775.     
  776.     return aData;
  777.   },
  778.   
  779.   observer: function(aThis)
  780.   {
  781.     return ({
  782.       _data: "",
  783.       
  784.       /**
  785.        * nsIStreamListener
  786.        */
  787.       onStartRequest: function(aRequest, aContext) {
  788.         this._data = "";
  789.       },
  790.       
  791.       onStopRequest: function(aRequest, aContext, aStatus) {
  792.         aThis.callback(this._data, aRequest);
  793.       },
  794.       
  795.       onDataAvailable: function(aRequest, aContext, aStream, aSourceOffset, aLength) {
  796.         var scriptableInputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
  797.         scriptableInputStream.init(aStream);
  798.         this._data += scriptableInputStream.read(aLength);
  799.       },
  800.       
  801.       /**
  802.        * nsIChannelEventSink
  803.        */
  804.       onChannelRedirect: function (aOldChannel, aNewChannel, aFlags) {
  805.         aThis._channel = aNewChannel;
  806.       },
  807.       
  808.       /**
  809.        * nsIProgressEventSink
  810.        */
  811.       onProgress: function (aRequest, aContext, aProgress, aProgressMax) { /* Stub */ },
  812.       onStatus: function (aRequest, aContext, aStatus, aStatusArg) { /* Stub */ },
  813.       
  814.       /**
  815.        * nsIHttpEventSink
  816.        */
  817.       onRedirect: function (aOldChannel, aNewChannel) { /* Stub */ },
  818.       
  819.       /**
  820.        * nsIInterfaceRequestor
  821.        */
  822.       getInterface: function(aIID) {
  823.         try {
  824.           return this.QueryInterface(aIID);
  825.         } catch(e) {
  826.           throw Components.results.NS_NOINTERFACE;
  827.         }
  828.       },
  829.       
  830.       QueryInterface: function(aIID) {
  831.         if (aIID.equals(Components.interfaces.nsIStreamListener) || 
  832.             aIID.equals(Components.interfaces.nsIChannelEventSink) || 
  833.             aIID.equals(Components.interfaces.nsIProgressEventSink) || 
  834.             aIID.equals(Components.interfaces.nsIHttpEventSink) || 
  835.             aIID.equals(Components.interfaces.nsIInterfaceRequestor) || 
  836.             aIID.equals(Components.interfaces.nsISupports))
  837.           return this;
  838.         throw Components.results.NS_ERROR_NO_INTERFACE;
  839.       }
  840.     });
  841.   },
  842.   
  843.   QueryInterface: function(iid)
  844.   {
  845.     if (iid.equals(Components.interfaces.gmIServiceGmail) || 
  846.         iid.equals(Components.interfaces.nsISupports))
  847.       return this;
  848.     throw Components.results.NS_ERROR_NO_INTERFACE;
  849.   }
  850. }
  851.  
  852. var myModule = {
  853.   firstTime: true,
  854.   
  855.   myCID: Components.ID("{b07df9d0-f7dd-11da-974d-0800200c9a66}"),
  856.   myDesc: "Gmail Account Service",
  857.   myProgID: "@longfocus.com/gmanager/service/gmail;1",
  858.   myFactory: {
  859.     createInstance: function (outer, iid) {
  860.       if (outer != null)
  861.         throw Components.results.NS_ERROR_NO_AGGREGATION;
  862.       
  863.       return (new gmServiceGmail()).QueryInterface(iid);
  864.     }
  865.   },
  866.   
  867.   registerSelf: function (compMgr, fileSpec, location, type)
  868.   {
  869.     if (this.firstTime) {
  870.       this.firstTime = false;
  871.       throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
  872.     }
  873.     
  874.     compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  875.     compMgr.registerFactoryLocation(this.myCID, this.myDesc, this.myProgID, fileSpec, location, type);
  876.   },
  877.   
  878.   getClassObject: function (compMgr, cid, iid)
  879.   {
  880.     if (!cid.equals(this.myCID))
  881.       throw Components.results.NS_ERROR_NO_INTERFACE;
  882.     
  883.     if (!iid.equals(Components.interfaces.nsIFactory))
  884.       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  885.     
  886.     return this.myFactory;
  887.   },
  888.   
  889.   canUnload: function(compMgr) { return true; }
  890. };
  891.  
  892. function NSGetModule(compMgr, fileSpec) { return myModule; }
  893.  
  894. /* ***** BEGIN LICENSE BLOCK *****
  895.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  896.  *
  897.  * The contents of this file are subject to the Mozilla Public License Version
  898.  * 1.1 (the "License"); you may not use this file except in compliance with
  899.  * the License. You may obtain a copy of the License at
  900.  * http://www.mozilla.org/MPL/
  901.  *
  902.  * Software distributed under the License is distributed on an "AS IS" basis,
  903.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  904.  * for the specific language governing rights and limitations under the
  905.  * License.
  906.  *
  907.  * The Original Code is Mozilla code.
  908.  *
  909.  * The Initial Developer of the Original Code is
  910.  * Simon B├╝nzli <zeniko@gmail.com>
  911.  * Portions created by the Initial Developer are Copyright (C) 2006-2007
  912.  * the Initial Developer. All Rights Reserved.
  913.  *
  914.  * Contributor(s):
  915.  *
  916.  * Alternatively, the contents of this file may be used under the terms of
  917.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  918.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  919.  * in which case the provisions of the GPL or the LGPL are applicable instead
  920.  * of those above. If you wish to allow use of your version of this file only
  921.  * under the terms of either the GPL or the LGPL, and not to allow others to
  922.  * use your version of this file under the terms of the MPL, indicate your
  923.  * decision by deleting the provisions above and replace them with the notice
  924.  * and other provisions required by the GPL or the LGPL. If you do not delete
  925.  * the provisions above, a recipient may use your version of this file under
  926.  * the terms of any one of the MPL, the GPL or the LGPL.
  927.  *
  928.  * ***** END LICENSE BLOCK ***** */
  929.  
  930. /**
  931.  * Utilities for JavaScript code to handle JSON content.
  932.  * See http://www.json.org/ for comprehensive information about JSON.
  933.  *
  934.  * Import this module through
  935.  *
  936.  * Components.utils.import("resource://gre/modules/JSON.jsm");
  937.  *
  938.  * Usage:
  939.  *
  940.  * var newJSONString = JSON.toString( GIVEN_JAVASCRIPT_OBJECT );
  941.  * var newJavaScriptObject = JSON.fromString( GIVEN_JSON_STRING );
  942.  *
  943.  * Note: For your own safety, Objects/Arrays returned by
  944.  *       JSON.fromString aren't instanceof Object/Array.
  945.  */
  946.  
  947. var EXPORTED_SYMBOLS = ["JSON"];
  948.  
  949. // The following code is a loose adaption of Douglas Crockford's code
  950. // from http://www.json.org/json.js (public domain'd)
  951.  
  952. // Notable differences:
  953. // * Unserializable values such as |undefined| or functions aren't
  954. //   silently dropped but always lead to a TypeError.
  955. // * An optional key blacklist has been added to JSON.toString
  956.  
  957. var JSON = {
  958.   /**
  959.    * Converts a JavaScript object into a JSON string.
  960.    *
  961.    * @param aJSObject is the object to be converted
  962.    * @param aKeysToDrop is an optional array of keys which will be
  963.    *                    ignored in all objects during the serialization
  964.    * @return the object's JSON representation
  965.    *
  966.    * Note: aJSObject MUST not contain cyclic references.
  967.    */
  968.   toString: function JSON_toString(aJSObject, aKeysToDrop) {
  969.     // we use a single string builder for efficiency reasons
  970.     var pieces = [];
  971.     
  972.     // this recursive function walks through all objects and appends their
  973.     // JSON representation (in one or several pieces) to the string builder
  974.     function append_piece(aObj) {
  975.       if (typeof aObj == "string") {
  976.         aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) {
  977.           // use the special escape notation if one exists, otherwise
  978.           // produce a general unicode escape sequence
  979.           switch ($0) {
  980.           case "\b": return "\\b";
  981.           case "\t": return "\\t";
  982.           case "\n": return "\\n";
  983.           case "\f": return "\\f";
  984.           case "\r": return "\\r";
  985.           case '"':  return '\\"';
  986.           case "\\": return "\\\\";
  987.           }
  988.           return "\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4);
  989.         });
  990.         pieces.push('"' + aObj + '"')
  991.       }
  992.       else if (typeof aObj == "boolean") {
  993.         pieces.push(aObj ? "true" : "false");
  994.       }
  995.       else if (typeof aObj == "number" && isFinite(aObj)) {
  996.         // there is no representation for infinite numbers or for NaN!
  997.         pieces.push(aObj.toString());
  998.       }
  999.       else if (aObj === null) {
  1000.         pieces.push("null");
  1001.       }
  1002.       // if it looks like an array, treat it as such - this is required
  1003.       // for all arrays from either outside this module or a sandbox
  1004.       else if (aObj instanceof Array ||
  1005.                typeof aObj == "object" && "length" in aObj &&
  1006.                (aObj.length === 0 || aObj[aObj.length - 1] !== undefined)) {
  1007.         pieces.push("[");
  1008.         for (var i = 0; i < aObj.length; i++) {
  1009.           arguments.callee(aObj[i]);
  1010.           pieces.push(",");
  1011.         }
  1012.         if (aObj.length > 0)
  1013.           pieces.pop(); // drop the trailing colon
  1014.         pieces.push("]");
  1015.       }
  1016.       else if (typeof aObj == "object") {
  1017.         pieces.push("{");
  1018.         for (var key in aObj) {
  1019.           // allow callers to pass objects containing private data which
  1020.           // they don't want the JSON string to contain (so they don't
  1021.           // have to manually pre-process the object)
  1022.           if (aKeysToDrop && aKeysToDrop.indexOf(key) != -1)
  1023.             continue;
  1024.           
  1025.           arguments.callee(key.toString());
  1026.           pieces.push(":");
  1027.           arguments.callee(aObj[key]);
  1028.           pieces.push(",");
  1029.         }
  1030.         if (pieces[pieces.length - 1] == ",")
  1031.           pieces.pop(); // drop the trailing colon
  1032.         pieces.push("}");
  1033.       }
  1034.       else {
  1035.         throw new TypeError("No JSON representation for this object!");
  1036.       }
  1037.     }
  1038.     append_piece(aJSObject);
  1039.     
  1040.     return pieces.join("");
  1041.   },
  1042.  
  1043.   /**
  1044.    * Converts a JSON string into a JavaScript object.
  1045.    *
  1046.    * @param aJSONString is the string to be converted
  1047.    * @return a JavaScript object for the given JSON representation
  1048.    */
  1049.   fromString: function JSON_fromString(aJSONString) {
  1050.     if (!this.isMostlyHarmless(aJSONString))
  1051.       throw new SyntaxError("No valid JSON string!");
  1052.     
  1053.     var s = new Components.utils.Sandbox("about:blank");
  1054.     return Components.utils.evalInSandbox("(" + aJSONString + ")", s);
  1055.   },
  1056.  
  1057.   /**
  1058.    * Checks whether the given string contains potentially harmful
  1059.    * content which might be executed during its evaluation
  1060.    * (no parser, thus not 100% safe! Best to use a Sandbox for evaluation)
  1061.    *
  1062.    * @param aString is the string to be tested
  1063.    * @return a boolean
  1064.    */
  1065.   isMostlyHarmless: function JSON_isMostlyHarmless(aString) {
  1066.     const maybeHarmful = /[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/;
  1067.     const jsonStrings = /"(\\.|[^"\\\n\r])*"/g;
  1068.     
  1069.     return !maybeHarmful.test(aString.replace(jsonStrings, ""));
  1070.   }
  1071. };